<!DOCTYPE html>
<html lang="en">
  
  <head>
    <meta charset="UTF-8">
    <title>Cloud Atlas Graph</title>
    <div id="mountNode"></div>
  </head>
  
  <body>
    <script src="../build/g6.js"></script>
    <script src="../build/minimap.js"></script>
    <script src="assets/hierarchy.js"></script>
    <script src="./assets/jquery-3.2.1.min.js"></script>
    <script>
      const ERROR_COLOR = "#F5222D";

      const SIMPLE_TREE_NODE = "simple-tree-node";
      const TREE_NODE = "tree-node";

      const SOFAROUTER_TEXT_CLASS = "sofarouter-text-class";
      const SOFAROUTER_RECT_CLASS = "sofarouter-rect-class";

      const CANVAS_WIDTH = 1000;
      const CANVAS_HEIGHT = 600;

      const LIMIT_OVERFLOW_WIDTH = CANVAS_WIDTH - 100;
      const LIMIT_OVERFLOW_HEIGHT = CANVAS_HEIGHT - 100;

      const TIP_HEIGHT = 28;
      const getNodeConfig = node => {
        if (node.nodeError) {
          return {
            basicColor: ERROR_COLOR,
            fontColor: "#FFF",
            borderColor: ERROR_COLOR,
            bgColor: "#E66A6C"
          };
        }
        let config = {
          basicColor: "#722ED1",
          fontColor: "#722ED1",
          borderColor: "#722ED1",
          bgColor: "#F6EDFC"
        };
        switch (node.type) {
        case "root":
          {
            config = {
              basicColor: "#E3E6E8",
              fontColor: "rgba(0,0,0,0.85)",
              borderColor: "#E3E6E8",
              bgColor: "#F7F9FA"
            };
            break;
          }
        case "httpclient":
        case "rest":
        case "mvc":
        case "rpc":
        case "rpc2jvm":
          config = {
            basicColor: "#2F54EB",
            fontColor: "#2F54EB",
            borderColor: "#2F54EB",
            bgColor: "#F3F6FD"
          };
          break;
        case "db":
          config = {
            basicColor: "#52C41A",
            fontColor: "#52C41A",
            borderColor: "#52C41A",
            bgColor: "#F4FCEB"
          };
          break;
        case "msgPub":
        case "msgSub":
        case "zqmsgSend":
        case "zqmsgRecv":
        case "antqPub":
        case "antqSub":
          config = {
            basicColor: "#FA8C16",
            fontColor: "#FA8C16",
            borderColor: "#FA8C16",
            bgColor: "#FCF4E3"
          };
          break;
        case "zdalTair":
        case "zdalOcs":
        case "zdalOss":
        default:
          break;
        }
        return config;
      };

      const COLLAPSE_ICON = function COLLAPSE_ICON(x, y, r) {
        return [["M", x - r, y], ["a", r, r, 0, 1, 0, r * 2, 0], ["a", r, r, 0, 1, 0, -r * 2, 0], ["M", x - r + 4, y], ["L", x - r + 2 * r - 4, y]];
      };
      const EXPAND_ICON = function EXPAND_ICON(x, y, r) {
        return [["M", x - r, y], ["a", r, r, 0, 1, 0, r * 2, 0], ["a", r, r, 0, 1, 0, -r * 2, 0], ["M", x - r + 4, y], ["L", x - r + 2 * r - 4, y], ["M", x - r + r, y - r + 4], ["L", x, y + r - 4]];
      };
      /* 精简节点和复杂节点共用的一些方法 */
      const nodeBasicMethod = {
        createNodeBox: (group, config, width, height, isRoot) => {
          /* 最外面的大矩形 */
          const container = group.addShape("rect", {
            attrs: {
              x: 0,
              y: 0,
              width,
              height
              // fill: '#FFF',
              // stroke: '#000',
            }
          });
          if (!isRoot) {
            /* 左边的小圆点 */
            group.addShape("circle", {
              attrs: {
                x: 3,
                y: height / 2,
                r: 6,
                fill: config.basicColor
              }
            });
          }
          /* 矩形 */
          group.addShape("rect", {
            attrs: {
              x: 3,
              y: 0,
              width: width - 19,
              height,
              fill: config.bgColor,
              stroke: config.borderColor,
              radius: 2,
              cursor: "pointer"
            }
          });

          /* 左边的粗线 */
          group.addShape("rect", {
            attrs: {
              x: 3,
              y: 0,
              width: 3,
              height,
              fill: config.basicColor,
              radius: 1.5
            }
          });
          return container;
        },
        /* 生成树上的 marker */
        createNodeMarker: (group, collapsed, x, y) => {
          group.addShape("circle", {
            attrs: {
              x,
              y,
              r: 13,
              fill: "rgba(47, 84, 235, 0.05)",
              opacity: 0,
              zIndex: -2
            },
            className: "collapse-icon-bg"
          });
          group.addShape("marker", {
            attrs: {
              x,
              y,
              radius: 7,
              symbol: collapsed ? EXPAND_ICON: COLLAPSE_ICON,
              stroke: "rgba(0,0,0,0.25)",
              fill: "rgba(0,0,0,0)",
              lineWidth: 1,
              cursor: "pointer"
            },
            className: "collapse-icon"
          });
        },
        afterDraw: (cfg, group) => {
          /* 操作 marker 的背景色显示隐藏 */
          const icon = group.findByClassName("collapse-icon");
          if (icon) {
            const bg = group.findByClassName("collapse-icon-bg");
            icon.on("mouseenter", () => {
              bg.attr("opacity", 1);
              graph.get("canvas").draw();
            });
            icon.on("mouseleave", () => {
              bg.attr("opacity", 0);
              graph.get("canvas").draw();
            });
          }
          /* ip 显示 */
          const ipBox = group.findByClassName("ip-box");
          if (ipBox) {
            /* ip 复制的几个元素 */
            const ipLine = group.findByClassName("ip-cp-line");
            const ipBG = group.findByClassName("ip-cp-bg");
            const ipIcon = group.findByClassName("ip-cp-icon");
            const ipCPBox = group.findByClassName("ip-cp-box");

            const onMouseEnter = () => {
              this.ipHideTimer && clearTimeout(this.ipHideTimer);
              ipLine.attr("opacity", 1);
              ipBG.attr("opacity", 1);
              ipIcon.attr("opacity", 1);
              graph.get("canvas").draw();
            };
            const onMouseLeave = () => {
              this.ipHideTimer = setTimeout(() => {
                ipLine.attr("opacity", 0);
                ipBG.attr("opacity", 0);
                ipIcon.attr("opacity", 0);
                graph.get("canvas").draw();
              },
              100);
            };
            ipBox.on("mouseenter", () => {
              onMouseEnter();
            });
            ipBox.on("mouseleave", () => {
              onMouseLeave();
            });
            ipCPBox.on("mouseenter", () => {
              onMouseEnter();
            });
            ipCPBox.on("mouseleave", () => {
              onMouseLeave();
            });
            ipCPBox.on("click", () => {});
          }
        },
        setState: (name, value, item) => {
          const hasOpacityClass = ["ip-cp-line", "ip-cp-bg", "ip-cp-icon", "ip-cp-box", "ip-box", "collapse-icon-bg"];
          const group = item.getContainer();
          const childrens = group.get("children");
          graph.setAutoPaint(false);
          if (name === "emptiness") {
            if (value) {
              childrens.forEach(shape => {
                if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
                  return;
                }
                shape.attr("opacity", 0.4);
              });
            } else {
              childrens.forEach(shape => {
                if (hasOpacityClass.indexOf(shape.get("className")) > -1) {
                  return;
                }
                shape.attr("opacity", 1);
              });
            }
          }
          graph.setAutoPaint(true);
        }
      };

      /* 精简节点 */
      G6.registerNode(SIMPLE_TREE_NODE, {
        drawShape: (cfg, group) => {
          const config = getNodeConfig(cfg);
          const isRoot = cfg.type === "root";
          const nodeError = cfg.nodeError;

          const container = nodeBasicMethod.createNodeBox(group, config, 171, 38, isRoot);

          /* name */
          const nameText = group.addShape("text", {
            attrs: {
              text: cfg.name,
              x: 19,
              y: 19,
              fontSize: 14,
              fontWeight: 700,
              textAlign: "left",
              textBaseline: "middle",
              fill: config.fontColor,
              cursor: "pointer"
            }
          });

          /* 修复 nameText 超长 */
          fittingString(nameText, cfg.name, 133);

          if (nodeError) {
            group.addShape("image", {
              attrs: {
                x: 119,
                y: 5,
                height: 35,
                width: 35,
                img: "/static/images/warning-circle.svg"
              }
            });
          }

          const hasChildren = cfg.children && cfg.children.length > 0;
          if (hasChildren) {
            nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 164, 19);
          }
          return container;
        },
        afterDraw: nodeBasicMethod.afterDraw,
        setState: nodeBasicMethod.setState
      },
      "single-shape");

      /* 复杂节点 */
      G6.registerNode(TREE_NODE, {
        drawShape: (cfg, group) => {
          const config = getNodeConfig(cfg);
          const isRoot = cfg.type === "root";
          const nodeError = cfg.nodeError;
          /* 最外面的大矩形 */
          const container = nodeBasicMethod.createNodeBox(group, config, 243, 64, isRoot);

          if (cfg.type !== "root") {
            /* 上边的 type */
            group.addShape("text", {
              attrs: {
                text: cfg.type,
                x: 3,
                y: -10,
                fontSize: 12,
                textAlign: "left",
                textBaseline: "middle",
                fill: "rgba(0,0,0,0.65)"
              }
            });
          }

          let ipWidth = 0;
          if (cfg.ip) {
            /* ip start */
            /* ipBox */
            const ipRect = group.addShape("rect", {
              attrs: {
                fill: nodeError ? null: "#FFF",
                stroke: nodeError ? "rgba(255,255,255,0.65)": null,
                radius: 2,
                cursor: "pointer"
              }
            });

            /* ip */
            const ipText = group.addShape("text", {
              attrs: {
                text: cfg.ip,
                x: 0,
                y: 19,
                fontSize: 12,
                textAlign: "left",
                textBaseline: "middle",
                fill: nodeError ? "rgba(255,255,255,0.85)": "rgba(0,0,0,0.65)",
                cursor: "pointer"
              }
            });

            const ipBBox = ipText.getBBox();
            /* ip 的文字总是距离右边 12px */
            ipText.attr({
              x: 224 - 12 - ipBBox.width
            });
            /* ipBox */
            ipRect.attr({
              x: 224 - 12 - ipBBox.width - 4,
              y: ipBBox.minY - 5,
              width: ipBBox.width + 8,
              height: ipBBox.height + 10
            });

            /* 在 IP 元素上面覆盖一层透明层，方便监听 hover 事件 */
            group.addShape("rect", {
              attrs: {
                stroke: "",
                cursor: "pointer",
                x: 224 - 12 - ipBBox.width - 4,
                y: ipBBox.minY - 5,
                width: ipBBox.width + 8,
                height: ipBBox.height + 10,
                fill: "#fff",
                opacity: 0
              },
              className: "ip-box"
            });

            /* copyIpLine */
            group.addShape("rect", {
              attrs: {
                x: 194,
                y: 7,
                width: 1,
                height: 24,
                fill: "#E3E6E8",
                opacity: 0
              },
              className: "ip-cp-line"
            });
            /* copyIpBG */
            group.addShape("rect", {
              attrs: {
                x: 195,
                y: 8,
                width: 22,
                height: 22,
                fill: "#FFF",
                cursor: "pointer",
                opacity: 0
              },
              className: "ip-cp-bg"
            });
            /* copyIpIcon */
            group.addShape("image", {
              attrs: {
                x: 200,
                y: 13,
                height: 12,
                width: 10,
                img: "https://os.alipayobjects.com/rmsportal/DFhnQEhHyPjSGYW.png",
                cursor: "pointer",
                opacity: 0
              },
              className: "ip-cp-icon"
            });
            /* 放一个透明的矩形在 icon 区域上，方便监听点击 */
            group.addShape("rect", {
              attrs: {
                x: 195,
                y: 8,
                width: 22,
                height: 22,
                fill: "#FFF",
                cursor: "pointer",
                opacity: 0
              },
              className: "ip-cp-box",
              tooltip: "复制IP"
            });

            const ipRectBBox = ipRect.getBBox();
            ipWidth = ipRectBBox.width;
            /* ip end */
          }

          /* name */
          const nameText = group.addShape("text", {
            attrs: {
              text: cfg.name,
              x: 19,
              y: 19,
              fontSize: 14,
              fontWeight: 700,
              textAlign: "left",
              textBaseline: "middle",
              fill: config.fontColor,
              cursor: "pointer"
            }
            // tooltip: cfg.name,
          });

          /* 根据 IP 的长度计算出 剩下的 留给 name 的长度！ */
          /* 修复 nameText 超长 */
          fittingString(nameText, cfg.name, 224 - ipWidth - 20);

          /* 下面的文字 */
          const remarkText = group.addShape("text", {
            attrs: {
              text: cfg.keyInfo,
              x: 19,
              y: 45,
              fontSize: 14,
              textAlign: "left",
              textBaseline: "middle",
              fill: config.fontColor,
              cursor: "pointer"
            }
            // className: 'keyInfo',
            // tooltip: cfg.keyInfo,
          });
          fittingString(remarkText, cfg.keyInfo, 204);

          if (nodeError) {
            group.addShape("image", {
              attrs: {
                x: 191,
                y: 32,
                height: 35,
                width: 35,
                img: "/static/images/warning-circle.svg"
              }
            });
          }

          const hasChildren = cfg.children && cfg.children.length > 0;
          if (hasChildren) {
            nodeBasicMethod.createNodeMarker(group, cfg.collapsed, 236, 32);
          }
          return container;
        },
        afterDraw: nodeBasicMethod.afterDraw,
        setState: nodeBasicMethod.setState
      },
      "single-shape");
      /* 是否显示 sofarouter，通过透明度来控制 */
      G6.registerEdge("tree-edge", {
        draw(cfg, group) {
          const targetNode = cfg.targetNode.getModel();
          const edgeError = !!targetNode.edgeError;

          const startPoint = cfg.startPoint;
          const endPoint = cfg.endPoint;
          const controlPoints = this.getControlPoints(cfg);
          let points = [startPoint]; // 添加起始点
          // 添加控制点
          if (controlPoints) {
            points = points.concat(controlPoints);
          }
          // 添加结束点
          points.push(endPoint);
          const path = this.getPath(points);

          group.addShape("path", {
            attrs: {
              path,
              lineWidth: 12,
              stroke: edgeError ? "rgba(245,34,45,0.05)": "rgba(47,84,235,0.05)",
              opacity: 0,
              zIndex: 0
            },
            className: "line-bg"
          });
          const keyShape = group.addShape("path", {
            attrs: {
              path,
              lineWidth: 1,
              stroke: edgeError ? "#FF7875": "rgba(0,0,0,0.25)",
              zIndex: 1,
              lineAppendWidth: 12
            },
            edgeError: !!edgeError
          });

          /* 连接线的中间点 */
          const centerPoint = {
            x: startPoint.x + (endPoint.x - startPoint.x) / 2,
            y: startPoint.y + (endPoint.y - startPoint.y) / 2
          };
          const textRect = group.addShape("rect", {
            attrs: {
              fill: "#FFF1F0",
              radius: 2,
              cursor: "pointer",
              opacity: 1
            },
            /* sofarouter 需要 class，以便控制 显示隐藏*/
            className: SOFAROUTER_RECT_CLASS
          });
          const text = group.addShape("text", {
            attrs: {
              text: "Sofarouter",
              x: 0,
              y: 0,
              fontSize: 12,
              textAlign: "left",
              textBaseline: "middle",
              fill: "#F5222D",
              opacity: 1
            },
            /* sofarouter 需要 class，以便控制 显示隐藏*/
            className: SOFAROUTER_TEXT_CLASS
          });
          const textBBox = text.getBBox();
          /* text 的位置 */
          text.attr({
            x: centerPoint.x - textBBox.width / 2,
            y: centerPoint.y
          });
          /* text 的框框 */
          textRect.attr({
            x: centerPoint.x - textBBox.width / 2 - 4,
            y: centerPoint.y - textBBox.height / 2 - 5,
            width: textBBox.width + 8,
            height: textBBox.height + 10
          });

          return keyShape;
        },
        /* 操作 线 的背景色显示隐藏 */
        afterDraw: (cfg, group) => {
          /* 背景色 */
          const lineBG = group.get("children")[0]; // 顺序根据 draw 时确定
          /* 线条 */
          const line = group.get("children")[1];
          line.on("mouseenter", () => {
            lineBG.attr("opacity", "1");
            /* 线条如果在没有错误的情况下，在 hover 时候，是需要变成蓝色的 */
            if (!line.get("edgeError")) {
              line.attr("stroke", "#2F54EB");
            }
            graph.get("canvas").draw();
          });
          line.on("mouseleave", () => {
            lineBG.attr("opacity", "0");
            if (!line.get("edgeError")) {
              line.attr("stroke", "rgba(0,0,0,0.25)");
            }
            graph.get("canvas").draw();
          });
        },
        setState: (name, value, item) => {
          const group = item.getContainer();
          const childrens = group.get("children");
          graph.setAutoPaint(true);
          if (name === "emptiness") {
            if (value) {
              childrens.forEach(shape => {
                if (shape.get("className") === "line-bg") {
                  return;
                }
                shape.attr("opacity", 0.4);
              });
            } else {
              childrens.forEach(shape => {
                if (shape.get("className") === "line-bg") {
                  return;
                }
                shape.attr("opacity", 1);
              });
            }
          }
          graph.setAutoPaint(true);
        },
        update: null
      },
      "cubic-horizontal");
      G6.registerBehavior("three-finger-drag-canvas", {
        getEvents() {
          return {
            "canvas:dragstart": "onDragStart",
            "canvas:drag": "onDrag",
            "canvas:dragend": "onDragEnd"
          };
        },
        onDragStart: ev => {
          ev.preventDefault();
          this.dragDx = ev.x;
          this.dragDy = ev.y;
        },
        onDrag: ev => {
          ev.preventDefault();
          translate(this.dragDx - ev.x, this.dragDy - ev.y);
        },
        onDragEnd: ev => {
          ev.preventDefault();
        }
      });
      G6.registerBehavior("double-finger-drag-canvas", {
        getEvents() {
          return {
            wheel: "onWheel"
          };
        },
        onWheel: ev => {
          if (ev.ctrlKey) {
            const canvas = graph.get("canvas");
            const point = canvas.getPointByClient(ev.clientX, ev.clientY);
            let ratio = graph.getZoom();
            if (ev.wheelDelta > 0) {
              ratio = ratio + ratio * 0.05;
            } else {
              ratio = ratio - ratio * 0.05;
            }
            graph.zoomTo(ratio, {
              x: point.x,
              y: point.y
            });
          } else {
            const x = ev.deltaX || ev.movementX;
            const y = ev.deltaY || ev.movementY;
            translate(x, y);
          }
          ev.preventDefault();
        }
      });
      /*G6.registerBehavior("tooltip", {
    getEvents() {
      return {
        "node:mousemove": "onMouseMove",
        "node:mouseeleave": "onMouseLeave"
      };
    },
    onMouseMove: ev => {
      const {
        tooltip: { visible }
      } = this.state;
      const tooltip = ev.target.get("tooltip");
      const group = ev.item.get("group");
      if (tooltip && !visible) {
        const bbox = ev.target.getBBox();
        const matrix = group.getMatrix();
        const zoom = graph.getZoom();
        this.showTooltip(
          tooltip,
          matrix[6] + bbox.x + bbox.width / 2,
          matrix[7] + bbox.y - TIP_HEIGHT / zoom
        );
      }
      if (!tooltip && visible) {
        this.closeTooptip();
      }
    },
    onMouseLeave: () => {
      const {
        tooltip: { visible }
      } = this.state;
      if (visible) {
        this.closeTooptip();
      }
    }
  });*/
      const minimap = new Minimap({
        size: [184, 124],
        className: "minimap",
        type: 'delegate'
      });
      let selectedItem;
      graph = new G6.TreeGraph({
        container: "mountNode",
        width: CANVAS_WIDTH,
        height: CANVAS_HEIGHT,
        plugins: [minimap],
        modes: {
          default: [{
            type:
            "collapse-expand",
            shouldUpdate: function shouldUpdate(e) {
              /* 点击 node 禁止展开收缩 */
              if (e.target.get("className") !== "collapse-icon") {
                return false;
              }
              return true;
            },
            onChange: function onChange(item, collapsed) {
              selectedItem = item;
              const icon = item.get("group").findByClassName("collapse-icon");
              if (collapsed) {
                icon.attr("symbol", EXPAND_ICON);
              } else {
                icon.attr("symbol", COLLAPSE_ICON);
              }
            },
            animate: {
              callback: () => {
                debugger;
                graph.focusItem(selectedItem);
              }
            }
          },
          "double-finger-drag-canvas", "three-finger-drag-canvas", "tooltip", {
            type: "drag-canvas",
            shouldUpdate: function shouldUpdate() {
              return false;
            },
            shouldEnd: function shouldUpdate() {
              return false;
            }
          }]
        },
        defaultNode: {
          shape: TREE_NODE,
          anchorPoints: [[0, 0.5], [1, 0.5]]
        },
        defaultEdge: {
          shape: "tree-edge",
          style: {
            stroke:
            "#A3B1BF"
          }
        },
        layout: data => {
          const result = Hierarchy.compactBox(data, {
            direction: "LR",
            getId: function getId(d) {
              return d.id;
            },
            getWidth: () => {
              return 243;
            },
            getVGap: function getVGap() {
              return 24;
            },
            getHGap: function getHGap() {
              return 50;
            }
          });
          return result;
        }
      });
      function strLen(str) {
        var len = 0;
        for (var i = 0; i < str.length; i++) {
          if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
            len++;
          } else {
            len += 2;
          }
        }
        return len;
      }
      function fittingString(c, str, maxWidth) {
        const width = strLen(str) * 8;
        const ellipsis = "…";
        if (width > maxWidth) {
          const actualLen = Math.floor((maxWidth - 10) / 8);
          c.attr('text', str.substring(0, actualLen) + ellipsis);
          c._cfg.tooltip = str;
        }
      };
      function translate(x, y) {
        // graph.translate(-x, -y);
        let moveX = x;
        let moveY = y;

        const containerWidth = graph.get("width");
        const containerHeight = graph.get("height");

        /* 获得当前偏移量*/
        const group = graph.get("group");
        const bbox = group.getBBox();
        const leftTopPoint = graph.getCanvasByPoint(bbox.minX, bbox.minY);
        const rightBottomPoint = graph.getCanvasByPoint(bbox.maxX, bbox.maxY);
        /* 如果 x 轴在区域内，不允许左右超过100 */
        if (x < 0 && leftTopPoint.x - x > LIMIT_OVERFLOW_WIDTH) {
          moveX = 0;
        }
        if (x > 0 && rightBottomPoint.x - x < containerWidth - LIMIT_OVERFLOW_WIDTH) {
          moveX = 0;
        }

        if (y < 0 && leftTopPoint.y - y > LIMIT_OVERFLOW_HEIGHT) {
          moveY = 0;
        }
        if (y > 0 && rightBottomPoint.y - y < containerHeight - LIMIT_OVERFLOW_HEIGHT) {
          moveY = 0;
        }
        graph.translate( - moveX, -moveY);
      };
      function fitView() {
        const group = graph.get("group");
        const width = graph.get("width"); // 视窗的宽度
        const height = graph.get("height"); // 视窗的高度
        group.resetMatrix();
        // 内容
        const bbox = group.getBBox();
        // 视窗中间
        const viewCenter = {
          x: width / 2,
          y: height / 2
        };
        /* 内容的中点 */
        const groupCenter = {
          x: bbox.x + bbox.width / 2,
          y: bbox.y + bbox.height / 2
        };
        graph.translate( - bbox.x + 16, viewCenter.y - groupCenter.y);
      };
      function zoomTo(ratio, center) {
        const width = graph.get("width"); // 视窗的宽度
        const height = graph.get("height"); // 视窗的高度
        const viewCenter = {
          x: width / 2,
          y: height / 2
        };
        graph.zoomTo(ratio, center || viewCenter);
      };
      function formatData(data) {
        const recursiveTraverse = (node, level = 0) => {
          // TODO 贤月 目前 keyInfo 没有
          const appName = "testappName";
          const keyInfo = "testkeyinfo";
          const ip = "111.22.33.44";

          const targetNode = {
            id: node.key + "",
            rpcId: node.rpcId,
            level,
            type: node.appName === "USER" ? "root": node.type,
            name: appName,
            keyInfo: keyInfo || "-",
            ip,
            nodeError: false,
            edgeError: false,
            sofarouter: true,
            sofarouterError: true,
            asyn: false,
            origin: node
          };
          if (node.children) {
            targetNode.children = [];
            node.children.forEach(item => {
              targetNode.children.push(recursiveTraverse(item, level + 1));
            });
          }
          return targetNode;
        };
        const result = recursiveTraverse(data);
        return result;
      }
      graph.on('beforepaint', () => {
        const topLeft = graph.getPointByCanvas(0, 0);
        const bottomRight = graph.getPointByCanvas(1000, 600);
        graph.getNodes().forEach(node => {
          const model = node.getModel();
          if (model.x < topLeft.x - 200 || model.x > bottomRight.x || model.y < topLeft.y || model.y > bottomRight.y) {
            node.getContainer().hide();
          } else {
            node.getContainer().show();
          }
        });
        const edges = graph.getEdges();
        edges.forEach(edge => {
          const sourceNode = edge.get('sourceNode');
          const targetNode = edge.get('targetNode');
          if (!sourceNode.get('visible') && !targetNode.get('visible')) {
            edge.hide();
          } else {
            edge.show();
          }
        })
      });
      $.getJSON('./assets/sourceData.json', data => {
        data = formatData(data);
        graph.data(data);
        graph.render();
        graph.fitView();
      });
    </script>
  </body>

</html>